/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver;
import java.beans.Introspector;
import java.lang.reflect.Method;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.IIOServiceProvider;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ImageWriterSpi;
import javax.media.jai.JAI;
import javax.media.jai.OperationRegistry;
import javax.media.jai.RegistryElementDescriptor;
import javax.media.jai.RegistryMode;
import javax.media.jai.remote.SerializableRenderedImage;
import javax.media.jai.util.ImagingListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.LogManager;
import org.geoserver.config.impl.CoverageAccessInfoImpl;
import org.geoserver.jai.ConcurrentOperationRegistry;
import org.geoserver.logging.LoggingUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.data.DataAccessFinder;
import org.geotools.data.DataStoreFinder;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.CRS;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.factory.AbstractAuthorityFactory;
import org.geotools.referencing.factory.DeferredAuthorityFactory;
import org.geotools.util.WeakCollectionCleaner;
import org.geotools.util.logging.Logging;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.FactoryException;
import it.geosolutions.concurrent.ConcurrentTileCache;
import it.geosolutions.concurrent.ConcurrentTileCacheMultiMap;
/**
* Listens for GeoServer startup and tries to configure axis order, logging
* redirection, and a few other things that really need to be set up before
* anything else starts up
*/
public class GeoserverInitStartupListener implements ServletContextListener {
private static final Logger LOGGER = Logging
.getLogger("org.geoserver.logging");
boolean relinquishLoggingControl;
private Iterator<Class<?>> products;
private final static String COMPARISON_TOLERANCE_PROPERTY = "COMPARISON_TOLERANCE";
private final static double DEFAULT_COMPARISON_TOLERANCE = 1e-8;
public void contextInitialized(ServletContextEvent sce) {
// start up tctool - remove it before committing!!!!
// new tilecachetool.TCTool().setVisible(true);
// Register logging, and bridge to JAI logging
GeoTools.init( (Hints) null );
// Custom GeoTools ImagingListener used to ignore common warnings
JAI.getDefaultInstance().setImagingListener(new ImagingListener() {
final Logger LOGGER = Logging.getLogger("javax.media.jai");
@Override
public boolean errorOccurred(String message, Throwable thrown, Object where,
boolean isRetryable) throws RuntimeException {
if (isSerializableRenderedImageFinalization(where, thrown)) {
LOGGER.log(Level.FINEST, message, thrown);
} else if (message.contains("Continuing in pure Java mode")) {
LOGGER.log(Level.FINE, message, thrown);
} else {
LOGGER.log(Level.INFO, message, thrown);
}
return false; // we are not trying to recover
}
private boolean isSerializableRenderedImageFinalization(Object where, Throwable t) {
if (!(where instanceof SerializableRenderedImage)) {
return false;
}
// check if it's the finalizer
StackTraceElement[] elements = t.getStackTrace();
for (StackTraceElement element : elements) {
if (element.getMethodName().equals("finalize")
&& element.getClassName().endsWith("SerializableRenderedImage"))
return true;
}
return false;
}
});
// setup concurrent operation registry
JAI jaiDef = JAI.getDefaultInstance();
if(!(jaiDef.getOperationRegistry() instanceof ConcurrentOperationRegistry ||
jaiDef.getOperationRegistry() instanceof it.geosolutions.jaiext.ConcurrentOperationRegistry)) {
jaiDef.setOperationRegistry(ConcurrentOperationRegistry.initializeRegistry());
}
// setup the concurrent tile cache (has proper memory limit handling also for small tiles)
if(!(jaiDef.getTileCache() instanceof ConcurrentTileCacheMultiMap)) {
jaiDef.setTileCache(new ConcurrentTileCacheMultiMap());
}
// make sure we remember if GeoServer controls logging or not
String strValue = GeoServerExtensions.getProperty(LoggingUtils.RELINQUISH_LOG4J_CONTROL,
sce.getServletContext());
relinquishLoggingControl = Boolean.valueOf(strValue);
// if the server admin did not set it up otherwise, force X/Y axis
// ordering
// This one is a good place because we need to initialize this property
// before any other opeation can trigger the initialization of the CRS
// subsystem
if (System.getProperty("org.geotools.referencing.forceXY") == null) {
System.setProperty("org.geotools.referencing.forceXY", "true");
}
if (Boolean.TRUE.equals(Hints
.getSystemDefault(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER))) {
Hints.putSystemDefault(Hints.FORCE_AXIS_ORDER_HONORING, "http");
}
Hints.putSystemDefault(Hints.LENIENT_DATUM_SHIFT, true);
// setup the referencing tolerance to make it more tolerant to tiny differences
// between projections (increases the chance of matching a random prj file content
// to an actual EPSG code
String comparisonToleranceProperty = GeoServerExtensions.getProperty(COMPARISON_TOLERANCE_PROPERTY);
double comparisonTolerance = DEFAULT_COMPARISON_TOLERANCE;
if (comparisonToleranceProperty != null) {
try {
comparisonTolerance = Double.parseDouble(comparisonToleranceProperty);
} catch (NumberFormatException nfe) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("Unable to parse the specified COMPARISON_TOLERANCE "
+ "system property: " + comparisonToleranceProperty +
" which should be a number. Using Default: " + DEFAULT_COMPARISON_TOLERANCE);
}
}
}
Hints.putSystemDefault(Hints.COMPARISON_TOLERANCE, comparisonTolerance);
final Hints defHints = GeoTools.getDefaultHints();
// Initialize GridCoverageFactory so that we don't make a lookup every time a factory is needed
Hints.putSystemDefault(Hints.GRID_COVERAGE_FACTORY,CoverageFactoryFinder.getGridCoverageFactory(defHints));
// don't allow the connection to the EPSG database to time out. This is a server app,
// we can afford keeping the EPSG db always on
System.setProperty("org.geotools.epsg.factory.timeout", "-1");
// HACK: java.util.prefs are awful. See
// http://www.allaboutbalance.com/disableprefs. When the site comes
// back up we should implement their better way of fixing the problem.
System.setProperty("java.util.prefs.syncInterval", "5000000");
// Fix issue with tomcat and JreMemoryLeakPreventionListener causing issues with
// IIORegistry leading to imageio plugins not being properly initialized
ImageIO.scanForPlugins();
// in any case, the native png reader is worse than the pure java ones, so
// let's disable it (the native png writer is on the other side faster)...
ImageIOExt.allowNativeCodec("png", ImageReaderSpi.class, false);
ImageIOExt.allowNativeCodec("png", ImageWriterSpi.class, true);
// initialize GeoTools factories so that we don't make a SPI lookup every time a factory is needed
Hints.putSystemDefault(Hints.FILTER_FACTORY, CommonFactoryFinder.getFilterFactory2(null));
Hints.putSystemDefault(Hints.STYLE_FACTORY, CommonFactoryFinder.getStyleFactory(null));
Hints.putSystemDefault(Hints.FEATURE_FACTORY, CommonFactoryFinder.getFeatureFactory(null));
// initialize the default executor service
final ThreadPoolExecutor executor = new ThreadPoolExecutor(CoverageAccessInfoImpl.DEFAULT_CorePoolSize,
CoverageAccessInfoImpl.DEFAULT_MaxPoolSize, CoverageAccessInfoImpl.DEFAULT_KeepAliveTime,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
Hints.putSystemDefault(Hints.EXECUTOR_SERVICE, executor);
}
/**
* This method tries hard to stop all threads and remove all references to classes in GeoServer
* so that we can avoid permgen leaks on application undeploy.
* What happes is that, if any JDK class references to one of the classes loaded by the
* webapp classloader, then the classloader cannot be collected and neither can all the
* classes loaded by it (since each class keeps a back reference to the classloader that
* loaded it). The same happens for any residual thread launched by the web app.
*/
public void contextDestroyed(ServletContextEvent sce) {
try {
LOGGER.info("Beginning GeoServer cleanup sequence");
// the dreaded classloader
ClassLoader webappClassLoader = getClass().getClassLoader();
// unload all of the jdbc drivers we have loaded. We need to store them and unregister
// later to avoid concurrent modification exceptions
Enumeration<Driver> drivers = DriverManager.getDrivers();
Set<Driver> driversToUnload = new HashSet<Driver>();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
try {
// the driver class loader can be null if the driver comes from the JDK, such as the
// sun.jdbc.odbc.JdbcOdbcDriver
ClassLoader driverClassLoader = driver.getClass().getClassLoader();
if (driverClassLoader != null && webappClassLoader.equals(driverClassLoader)) {
driversToUnload.add(driver);
}
} catch(Throwable t) {
t.printStackTrace();
}
}
for (Driver driver : driversToUnload) {
try {
DriverManager.deregisterDriver(driver);
LOGGER.info("Unregistered JDBC driver " + driver);
} catch(Exception e) {
LOGGER.log(Level.SEVERE, "Could now unload driver " + driver.getClass(), e);
}
}
drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
}
try {
Class h2Driver = Class.forName("org.h2.Driver");
Method m = h2Driver.getMethod("unload");
m.invoke(null);
} catch(Exception e) {
LOGGER.log(Level.WARNING, "Failed to unload the H2 driver", e);
}
// unload all deferred authority factories so that we get rid of the timer tasks in them
try {
disposeAuthorityFactories(ReferencingFactoryFinder.getCoordinateOperationAuthorityFactories(null));
} catch (Throwable e) {
LOGGER.log(Level.WARNING, "Error occurred trying to dispose authority factories", e);
}
try {
disposeAuthorityFactories(ReferencingFactoryFinder.getCRSAuthorityFactories(null));
} catch (Throwable e) {
LOGGER.log(Level.WARNING, "Error occurred trying to dispose authority factories", e);
}
try {
disposeAuthorityFactories(ReferencingFactoryFinder.getCSAuthorityFactories(null));
} catch (Throwable e) {
LOGGER.log(Level.WARNING, "Error occurred trying to dispose authority factories", e);
}
// kill the threads created by referencing
WeakCollectionCleaner.DEFAULT.exit();
DeferredAuthorityFactory.exit();
CRS.reset("all");
LOGGER.info("Shut down GT referencing threads ");
// reset
ReferencingFactoryFinder.reset();
CommonFactoryFinder.reset();
DataStoreFinder.reset();
DataAccessFinder.reset();
LOGGER.info("Shut down GT SPI ");
LOGGER.info("Shut down coverage thread pool ");
Object o =Hints.getSystemDefault(Hints.EXECUTOR_SERVICE);
if(o !=null && o instanceof ExecutorService){
final ThreadPoolExecutor executor = (ThreadPoolExecutor) o;
try{
executor.shutdown();
} finally {
try {
executor.shutdownNow();
} finally {
}
}
}
// unload everything that JAI ImageIO can still refer to
// We need to store them and unregister later to avoid concurrent modification exceptions
final IIORegistry ioRegistry = IIORegistry.getDefaultInstance();
Set<IIOServiceProvider> providersToUnload = new HashSet();
for(Iterator<Class<?>> cats = ioRegistry.getCategories(); cats.hasNext(); ) {
Class<?> category = cats.next();
for (Iterator it = ioRegistry.getServiceProviders(category, false); it.hasNext();) {
final IIOServiceProvider provider = (IIOServiceProvider) it.next();
if(webappClassLoader.equals(provider.getClass().getClassLoader())) {
providersToUnload.add(provider);
}
}
}
for (IIOServiceProvider provider : providersToUnload) {
ioRegistry.deregisterServiceProvider(provider);
LOGGER.info("Unregistering Image I/O provider " + provider);
}
// unload everything that JAI can still refer to
final OperationRegistry opRegistry = JAI.getDefaultInstance().getOperationRegistry();
for(String mode : RegistryMode.getModeNames()) {
for (Iterator descriptors = opRegistry.getDescriptors(mode).iterator(); descriptors != null && descriptors.hasNext();) {
RegistryElementDescriptor red = (RegistryElementDescriptor) descriptors.next();
int factoryCount = 0;
int unregisteredCount = 0;
// look for all the factories for that operation
for (Iterator factories = opRegistry.getFactoryIterator(mode, red.getName()); factories != null && factories.hasNext();) {
Object factory = factories.next();
if(factory == null) {
continue;
}
factoryCount++;
if(webappClassLoader.equals(factory.getClass().getClassLoader())) {
boolean unregistered = false;
// we need to scan against all "products" to unregister the factory
Vector orderedProductList = opRegistry.getOrderedProductList(mode, red.getName());
if(orderedProductList != null) {
for(Iterator products = orderedProductList.iterator(); products != null && products.hasNext();) {
String product = (String) products.next();
try {
opRegistry.unregisterFactory(mode, red.getName(), product, factory);
LOGGER.info("Unregistering JAI factory " + factory.getClass());
} catch(Throwable t) {
// may fail due to the factory not being registered against that product
}
}
}
if(unregistered) {
unregisteredCount++;
}
}
}
// if all the factories were unregistered, get rid of the descriptor as well
if(factoryCount > 0 && unregisteredCount == factoryCount) {
opRegistry.unregisterDescriptor(red);
}
}
}
// flush all javabean introspection caches as this too can keep a webapp classloader from being unloaded
Introspector.flushCaches();
LOGGER.info("Cleaned up javabean caches");
// unload the logging framework
if(!relinquishLoggingControl)
LogManager.shutdown();
LogFactory.release(Thread.currentThread().getContextClassLoader());
// GeoTools/GeoServer have a lot of finalizers and until they are run the JVM
// itself wil keepup the class loader...
try {
System.gc();
System.runFinalization();
System.gc();
System.runFinalization();
System.gc();
System.runFinalization();
} catch(Throwable t) {
LOGGER.severe("Failed to perform closing up finalization");
t.printStackTrace();
}
} catch(Throwable t) {
// if anything goes south during the cleanup procedures I want to know what it is
t.printStackTrace();
}
}
private void disposeAuthorityFactories(Set<? extends AuthorityFactory> factories)
throws FactoryException {
for (AuthorityFactory af : factories) {
if(af instanceof AbstractAuthorityFactory) {
LOGGER.info("Disposing referencing factory " + af);
((AbstractAuthorityFactory) af).dispose();
}
}
}
}